package com.wcs.exam.controller.auth.biz;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.plugins.pagination.PageDTO;
import com.wcs.exam.common.base.BaseBiz;
import com.wcs.exam.common.base.Result;
import com.wcs.exam.common.base.exception.BaseException;
import com.wcs.exam.common.base.page.Page;
import com.wcs.exam.common.base.page.PageUtil;
import com.wcs.exam.common.base.page.SqlUtil;
import com.wcs.exam.common.config.ThreadContext;
import com.wcs.exam.common.constant.RedisConstant;
import com.wcs.exam.common.enums.*;
import com.wcs.exam.common.util.BeanUtil;
import com.wcs.exam.controller.auth.req.*;
import com.wcs.exam.controller.auth.resp.*;
import com.wcs.exam.controller.biz.CategoryCommonBiz;
import com.wcs.exam.controller.biz.ExamCommonBiz;
import com.wcs.exam.controller.biz.QuestionCommonBiz;
import com.wcs.exam.controller.biz.resp.ExamCommonResp;
import com.wcs.exam.controller.biz.resp.QuestionDetailResp;
import com.wcs.exam.dao.*;
import com.wcs.exam.dao.impl.mapper.entity.*;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * Auth-考试
 *
 * @author wcs
 * @since 2024-07-10
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class AuthExamBiz extends BaseBiz {

    @NotNull
    private final RedissonClient redissonClient;
    @NotNull
    private final ThreadPoolTaskExecutor threadPoolTaskExecutor;

    @NotNull
    private final ExamCommonBiz examCommonBiz;
    @NotNull
    private final QuestionCommonBiz questionCommonBiz;
    @NotNull
    private final CategoryCommonBiz categoryCommonBiz;

    @NotNull
    private final ExamDao dao;
    @NotNull
    private final ExamUserDao examUserDao;
    @NotNull
    private final ExamUserRecordDao examUserRecordDao;
    @NotNull
    private final ExamUserAnswerDao examUserAnswerDao;
    @NotNull
    private final ExamUserAppealDao examUserAppealDao;
    @NotNull
    private final PaperDao paperDao;
    @NotNull
    private final PaperTitleDao paperTitleDao;
    @NotNull
    private final UserAttendanceDao userAttendanceDao;
    @NotNull
    private final UsersDao usersDao;


    /**
     * 考试-分页
     *
     * @param req 分页查询参数
     * @return 考试
     */
    public Result<Page<AuthExamPageResp>> search(AuthExamPageReq req) {
        // 分类ID
        Set<Long> categoryIds = new HashSet<>();
        if (ObjectUtil.isNotNull(req.getCategoryId())) {
            categoryIds.add(req.getCategoryId());
            List<Long> childCategoryIds = categoryCommonBiz.listChildrenIdById(req.getCategoryId());
            if (CollectionUtil.isNotEmpty(childCategoryIds)) {
                categoryIds.addAll(childCategoryIds);
            }
        }

        LambdaQueryWrapper<Exam> wrapper = new LambdaQueryWrapper<Exam>()
                .eq(ObjectUtil.isNotNull(req.getExamType()), Exam::getExamType, req.getExamType())
                .like(StrUtil.isNotBlank(req.getExamName()), Exam::getExamName, req.getExamName())
                .in(CollectionUtil.isNotEmpty(categoryIds), Exam::getCategoryId, categoryIds)
                .like(StrUtil.isNotBlank(req.getDescription()), Exam::getDescription, req.getDescription())
                .eq(Exam::getPublishStatus, PublishStatusEnum.PUBLISHED.getCode())
                .eq(Exam::getStatus, StatusEnum.NORMAL.getCode());
        // 收费情况
        if (ObjectUtil.isNotNull(req.getFree())) {
            if (Boolean.TRUE.equals(req.getFree())) {
                wrapper.eq(Exam::getPrice, BigDecimal.ZERO);
            } else {
                wrapper.ne(Exam::getPrice, BigDecimal.ZERO);
            }
        }
        // 处理报名状态
        if (ObjectUtil.isNotNull(req.getRegistrationStatus())) {
            if (req.getRegistrationStatus() == 1) {
                // 未开始
                wrapper.gt(Exam::getRegistrationStartTime, LocalDateTime.now());
            } else if (req.getRegistrationStatus() == 2) {
                // 报名中
                wrapper.le(Exam::getRegistrationStartTime, LocalDateTime.now()).ge(Exam::getRegistrationEndTime, LocalDateTime.now());
            } else if (req.getRegistrationStatus() == 3) {
                // 已结束
                wrapper.lt(Exam::getRegistrationEndTime, LocalDateTime.now());
            } else {
                return Result.success(new Page<>());
            }
        }

        // 处理排序
        wrapper.orderByDesc(Exam::getSort, Exam::getId);

        IPage<Exam> page = dao.page(PageDTO.of(req.getPageCurrent(), req.getPageSize()), wrapper);
        if (ObjectUtil.isNull(page) || CollectionUtil.isEmpty(page.getRecords())) {
            return Result.success(new Page<>());
        }
        Page<AuthExamPageResp> respPage = PageUtil.transform(page, AuthExamPageResp.class);

        // 处理分页结果
        for (AuthExamPageResp resp : respPage.getList()) {
            // 处理报名状态
            if (ExamTypeEnum.EXAMINATION.getCode().equals(resp.getExamType())) {
                if (LocalDateTime.now().isBefore(resp.getRegistrationStartTime())) {
                    resp.setRegistrationStatus(1);
                } else if (!LocalDateTime.now().isBefore(resp.getRegistrationStartTime()) && !LocalDateTime.now().isAfter(resp.getRegistrationEndTime())) {
                    resp.setRegistrationStatus(2);
                } else {
                    resp.setRegistrationStatus(3);
                }
            } else {
                resp.setRegistrationStatus(1);
            }
        }
        return Result.success(respPage);
    }

    /**
     * 考试-查看
     *
     * @param id 考试ID
     * @return 考试
     */
    public Result<AuthExamViewResp> view(Long id) {
        Exam exam = dao.getById(id);
        if (ObjectUtil.isNull(exam)) {
            return Result.error("考试不存在");
        }
        AuthExamViewResp resp = BeanUtil.copyProperties(exam, AuthExamViewResp.class);

        Paper paper = paperDao.getById(resp.getPaperId());
        if (ObjectUtil.isNotNull(paper)) {
            resp.setTotalScore(paper.getTotalScore());
            resp.setQuestionCount(paper.getQuestionCount());
        }

        resp.setRegistrationUserStatus(Boolean.FALSE);
        resp.setPayment(Boolean.FALSE);
        ExamUser examUser = examUserDao.lambdaQuery().eq(ExamUser::getExamId, exam.getId()).eq(ExamUser::getUserId, ThreadContext.getUserId()).one();
        if (ExamTypeEnum.EXAMINATION.getCode().equals(exam.getExamType())) {
            if (ObjectUtil.isNotNull(examUser)) {
                resp.setRegistrationUserStatus(Boolean.TRUE);
                resp.setUserEvaluationStatus(examUser.getUserEvaluationStatus());
                resp.setUserExamStatus(examUser.getUserExamStatus());
                resp.setExamRecordId(examUser.getExamRecordId());
                resp.setPayment(examUser.getPayment());
            }
        } else {
            if (ObjectUtil.isNotNull(examUser)) {
                resp.setPayment(examUser.getPayment());
                if (Boolean.TRUE.equals(examUser.getPayment())) {
                    if (Boolean.TRUE.equals(examUser.getLongEffective())) {
                        resp.setValidityDateType(ValidityDateTypeEnum.LONG_EFFECTIVE.getCode());
                        resp.setBeginTime(null);
                        resp.setEndTime(null);
                    } else {
                        resp.setValidityDateType(ValidityDateTypeEnum.FIXED_PERIOD.getCode());
                        resp.setBeginTime(examUser.getBeginTime());
                        resp.setEndTime(examUser.getEndTime());
                    }
                }
            }
        }

        // 处理报名状态
        if (ExamTypeEnum.EXAMINATION.getCode().equals(resp.getExamType())) {
            if (LocalDateTime.now().isBefore(resp.getRegistrationStartTime())) {
                resp.setRegistrationStatus(1);
            } else if (!LocalDateTime.now().isBefore(resp.getRegistrationStartTime()) && !LocalDateTime.now().isAfter(resp.getRegistrationEndTime())) {
                resp.setRegistrationStatus(2);
            } else {
                resp.setRegistrationStatus(3);
            }
        } else {
            resp.setRegistrationStatus(1);
        }


        // 判断是否能继续考试
        ExamCommonBiz.ExamCheckContinue examCheckContinue = examCommonBiz.checkCanContinueExam(ThreadContext.getUserId(), exam.getExamType(), exam.getId());
        resp.setStartExam(examCheckContinue.getStartExam());
        resp.setUserExamCount(examCheckContinue.getUserExamCount());
        resp.setResidueExamCount(examCheckContinue.getResidueExamCount());
        return Result.success(resp);
    }

    /**
     * 考试-报名
     *
     * @param req 报名参数
     * @return 报名结果
     */
    public Result<String> registration(AuthExamRegistrationReq req) {
        Exam exam = dao.getById(req.getExamId());
        if (ObjectUtil.isNull(exam)) {
            return Result.error("考试不存在");
        }
        if (!ExamTypeEnum.EXAMINATION.getCode().equals(exam.getExamType())) {
            return Result.error("考试类型错误");
        }
        if (exam.getRegistrationStartTime().isAfter(LocalDateTime.now()) || exam.getRegistrationEndTime().isBefore(LocalDateTime.now())) {
            return Result.error("报名尚未开始/已结束，无法进行报名");
        }

        ExamUser examUser = examUserDao.getByExamIdAndUserId(req.getExamId(), ThreadContext.getUserId());
        if (ObjectUtil.isNotNull(examUser)) {
            if (Boolean.TRUE.equals(examUser.getLongEffective()) || !examUser.getEndTime().isBefore(LocalDateTime.now())) {
                return Result.error("考试用户已存在,无需重复报名");
            }

            // 过期则更新
            ExamUser updateExamUser = new ExamUser();
            updateExamUser.setId(examUser.getId());
            if (ValidityDateTypeEnum.LONG_EFFECTIVE.getCode().equals(exam.getValidityDateType())) {
                // 长期有效
                updateExamUser.setLongEffective(Boolean.TRUE);
            } else {
                // 非长期有效
                updateExamUser.setLongEffective(Boolean.FALSE);
                if (ValidityDateTypeEnum.FIXED_PERIOD.getCode().equals(exam.getValidityDateType())) {
                    updateExamUser.setBeginTime(exam.getBeginTime());
                    updateExamUser.setEndTime(exam.getEndTime());
                } else if (ValidityDateTypeEnum.FIXED_DAY.getCode().equals(exam.getValidityDateType())) {
                    // 如果是绑定中的状态
                    if (examUser.getEndTime().isAfter(LocalDateTime.now())) {
                        // 结束时间在当前时间之后则直接进行时间累加
                        updateExamUser.setEndTime(examUser.getEndTime().plusDays(exam.getDays()));
                    } else {
                        // 否则进行重新计算
                        updateExamUser.setBeginTime(LocalDateTime.now());
                        updateExamUser.setEndTime(examUser.getBeginTime().plusDays(exam.getDays()));
                    }
                }
            }
            updateExamUser.setPayment(Boolean.TRUE);
            updateExamUser.setRegistrationWay(ExamRegistrationWayEnum.AUTO_REGISTER.getCode());
            updateExamUser.setRegistrationTime(LocalDateTime.now());
            updateExamUser.setOrderNo("");
            examUserDao.updateById(updateExamUser);
            return Result.success(toI18nSuccess());
        } else {
            // 不存在，创建新的关联记录
            ExamUser saveExamUser = new ExamUser();
            saveExamUser.setUserId(ThreadContext.getUserId());
            saveExamUser.setExamId(exam.getId());
            saveExamUser.setExamType(exam.getExamType());
            if (ValidityDateTypeEnum.LONG_EFFECTIVE.getCode().equals(exam.getValidityDateType())) {
                saveExamUser.setLongEffective(Boolean.TRUE);
            } else {
                saveExamUser.setLongEffective(Boolean.FALSE);
                if (ValidityDateTypeEnum.FIXED_PERIOD.getCode().equals(exam.getValidityDateType())) {
                    saveExamUser.setBeginTime(exam.getBeginTime());
                    saveExamUser.setEndTime(exam.getEndTime());
                } else if (ValidityDateTypeEnum.FIXED_DAY.getCode().equals(exam.getValidityDateType())) {
                    saveExamUser.setBeginTime(LocalDateTime.now());
                    saveExamUser.setEndTime(saveExamUser.getBeginTime().plusDays(exam.getDays()));
                }
            }

            saveExamUser.setPayment(Boolean.TRUE);
            saveExamUser.setRegistrationWay(ExamRegistrationWayEnum.AUTO_REGISTER.getCode());
            saveExamUser.setRegistrationTime(LocalDateTime.now());
            saveExamUser.setOrderNo("");
            examUserDao.save(saveExamUser);
            return Result.success(toI18nSuccess());
        }
    }

    /**
     * 考试-检查
     *
     * @param req 校验参数
     * @return 检查结果
     */
    public Result<AuthExamCheckResp> check(AuthExamCheckReq req) {
        ExamTypeEnum examTypeEnum = ExamTypeEnum.getByCode(req.getExamType());
        if (ObjectUtil.isNull(examTypeEnum)) {
            return Result.error("考试类型错误");
        }

        Users user = usersDao.getById(ThreadContext.getUserId());
        if (ObjectUtil.isNull(user)) {
            return Result.error("用户信息不存在");
        }

        if (ExamTypeEnum.MOCK.equals(examTypeEnum)) {
            // 模拟考试
            return checkGeneralExam(req.getRelateId());
        } else if (ExamTypeEnum.EXAMINATION.equals(examTypeEnum)) {
            // 正式考试
            return checkRegistrationExam(req.getRelateId());
        }

        // 其他
        return Result.error("该考试类型暂不支持");
    }

    /**
     * 模拟考试-检查
     *
     * @param relateId 考试关联ID
     * @return 校验结果
     */
    private Result<AuthExamCheckResp> checkGeneralExam(Long relateId) {
        Exam exam = dao.getByIdForCache(relateId);
        if (ObjectUtil.isNull(exam)) {
            return Result.error("考试不存在");
        }

        Paper paper = paperDao.getByIdForCache(exam.getPaperId());
        if (ObjectUtil.isNull(paper)) {
            return Result.error("试卷不存在");
        }

        if (ValidityDateTypeEnum.FIXED_PERIOD.getCode().equals(exam.getValidityDateType())) {
            // 固定周期
            LocalDateTime now = LocalDateTime.now();
            if (now.isBefore(exam.getBeginTime()) || now.isAfter(exam.getEndTime())) {
                return Result.error("不在有效时间范围内，不允许考试");
            }
        }

        return checkUserExamStatus(ExamTypeEnum.MOCK.getCode(), exam.getExamName(), exam.getId());
    }

    /**
     * 正式考试-检查
     *
     * @param relateId 考试关联ID
     * @return 校验结果
     */
    private Result<AuthExamCheckResp> checkRegistrationExam(Long relateId) {
        Exam exam = dao.getById(relateId);
        if (ObjectUtil.isNull(exam)) {
            return Result.error("考试不存在");
        }
        return checkUserExamStatus(ExamTypeEnum.EXAMINATION.getCode(), exam.getExamName(), exam.getId());
    }


    /**
     * 判断用户是否处于考试中
     *
     * @return 校验结果
     */
    public Result<AuthExamCheckResp> checkUserExamStatus(Integer examType, String examName, Long relateId) {
        AuthExamCheckResp resp = new AuthExamCheckResp();
        resp.setExamName(examName);

        // 判断当前用户是否在考试
        ExamUserRecord examUserRecord = examUserRecordDao.lambdaQuery()
                .eq(ExamUserRecord::getUserId, ThreadContext.getUserId())
                .eq(ExamUserRecord::getExamType, examType)
                .eq(ExamUserRecord::getRelateId, relateId)
                .eq(ExamUserRecord::getExamStatus, ExamStatusEnum.NOT_OVER.getCode())
                .gt(ExamUserRecord::getEndTime, LocalDateTime.now())
                .orderByDesc(ExamUserRecord::getId)
                .last(SqlUtil.limitOne())
                .one();

        if (ObjectUtil.isNotNull(examUserRecord)) {
            resp.setIsExamination(true);
            resp.setRecordId(examUserRecord.getId());
            resp.setExamType(examUserRecord.getExamType());
        } else {
            resp.setIsExamination(false);
        }
        return Result.success(resp);
    }


    /**
     * 开始考试
     *
     * @param req 开始考试参数
     * @return 考试信息
     */
    @Transactional(rollbackFor = Exception.class)
    public Result<AuthExamStartResp> examStart(AuthExamStartReq req) {
        // 加等待锁-(处理完考试记录才会解锁)
        RLock lock = redissonClient.getLock(RedisConstant.Exam.EXAM_START + ThreadContext.getUserId() + RedisConstant.SEPARATOR + req.getExamType());
        try {
            boolean examLock = lock.tryLock(RedisConstant.DEFAULT_WAIT_TIME, RedisConstant.DEFAULT_LEASE_TIME, TimeUnit.SECONDS);
            if (!examLock) {
                log.warn("用户考试正在处理");
                return Result.error("等待超时");
            }

            if (ExamTypeEnum.MOCK.getCode().equals(req.getExamType())) {
                // 模拟考试
                return generalExamStart(req);
            } else if (ExamTypeEnum.EXAMINATION.getCode().equals(req.getExamType())) {
                // 正式考试
                return registrationExamStart(req);
            } else {
                return Result.error("该考试类型暂不支持");
            }
        } catch (InterruptedException e) {
            log.warn("处理用户考试记录报错", e);
            Thread.currentThread().interrupt();
            return Result.error(toI18nFail());
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

    /**
     * 模拟考试
     *
     * @param req 开始考试参数
     * @return 开考响应参数
     */
    private Result<AuthExamStartResp> generalExamStart(AuthExamStartReq req) {
        // 获取模拟试卷信息
        Exam exam = dao.getById(req.getRelateId());
        if (ObjectUtil.isNull(exam) || !ExamTypeEnum.MOCK.getCode().equals(exam.getExamType())) {
            return Result.error("考试不存在");
        }

        ExamUser examUser = examUserDao.getByExamTypeAndExamIdAndUserId(ExamTypeEnum.MOCK.getCode(), exam.getId(), ThreadContext.getUserId());
        if (BigDecimal.ZERO.compareTo(exam.getPrice()) < 0) {
            if (Boolean.FALSE.equals(examUser.getPayment())) {
                return Result.error("考试未购买");
            }
            if (Boolean.FALSE.equals(examUser.getLongEffective())) {
                if (LocalDateTime.now().isBefore(examUser.getBeginTime())) {
                    return Result.error("该考试暂未开始，暂时无法进行考试");
                }
                if (LocalDateTime.now().isAfter(examUser.getEndTime())) {
                    return Result.error("该考试已过期，无法进行考试");
                }
            }
        } else {
            // 创建关联记录
            if (ObjectUtil.isNull(examUser)) {
                Boolean longEffective = Boolean.FALSE;
                LocalDateTime beginTime = null;
                LocalDateTime endTime = null;
                if (ValidityDateTypeEnum.LONG_EFFECTIVE.getCode().equals(exam.getValidityDateType())) {
                    longEffective = Boolean.TRUE;
                } else if (ValidityDateTypeEnum.FIXED_DAY.getCode().equals(exam.getValidityDateType())) {
                    beginTime = LocalDateTime.now();
                    endTime = beginTime.plusDays(exam.getDays());
                } else {
                    beginTime = exam.getBeginTime();
                    endTime = exam.getEndTime();
                }

                examUser = new ExamUser()
                        .setUserId(ThreadContext.getUserId())
                        .setExamId(exam.getId())
                        .setExamType(exam.getExamType())
                        .setLongEffective(longEffective)
                        .setBeginTime(beginTime)
                        .setEndTime(endTime)
                        .setPayment(Boolean.FALSE)
                        .setRegistrationWay(ExamRegistrationWayEnum.AUTO_REGISTER.getCode())
                        .setExamScoreType(exam.getExamScoreType())
                        .setUserExamStatus(UserExamStatusEnum.WAIT_EXAM.getCode())
                        .setUserEvaluationStatus(UserEvaluationStatusEnum.UN_KNOW.getCode());
                examUserDao.save(examUser);
            }
        }

        // 校验考试次数
        if (exam.getExamCount() > 0) {
            Long userExamCount = examUserRecordDao.lambdaQuery()
                    .eq(ExamUserRecord::getUserId, ThreadContext.getUserId())
                    .eq(ExamUserRecord::getExamType, exam.getExamType())
                    .eq(ExamUserRecord::getRelateId, exam.getId())
                    .count();
            if (userExamCount >= exam.getExamCount()) {
                return Result.error("已经考试了" + userExamCount + "遍，达到了可考试的最大次数");
            }
        }

        // 处理人脸校验
        if (Boolean.TRUE.equals(exam.getFaceId())) {
            boolean checkResult = userAttendanceDao.lambdaQuery()
                    .eq(UserAttendance::getUserId, ThreadContext.getUserId())
                    .eq(UserAttendance::getAttendanceType, AttendanceTypeEnum.GENERAL_EXAM.getCode())
                    .eq(UserAttendance::getAttendanceExamScene, AttendanceExamSceneEnum.EXAM_BEFORE.getCode())
                    .eq(UserAttendance::getRelateId, exam.getId())
                    .eq(UserAttendance::getAttendancePass, Boolean.TRUE)
                    .and(q -> q
                            .isNull(UserAttendance::getExamRecordId)
                            .or()
                            .eq(UserAttendance::getExamRecordId, 0L)
                    ).exists();
            if (!checkResult) {
                return Result.error("考试人脸识别未通过");
            }
        }

        // 获取试卷信息
        Paper paper = paperDao.getByIdForCache(exam.getPaperId());
        if (ObjectUtil.isNull(paper)) {
            return Result.error("试卷不存在");
        }

        ExamCommonResp examDetail = examCommonBiz.getExamDetail(paper);
        AuthExamStartResp startExamResp = BeanUtil.copyProperties(examDetail, AuthExamStartResp.class);

        LocalDateTime endTime = LocalDateTime.now().plusMinutes(exam.getAnswerTime());
        if (Boolean.FALSE.equals(examUser.getLongEffective())) {
            if (endTime.isAfter(examUser.getEndTime())) {
                endTime = examUser.getEndTime();
            }
        }

        // 计算合格分
        BigDecimal passScore = paper.getTotalScore()
                .multiply(BigDecimal.valueOf(exam.getPassPercentage()))
                .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);

        Long recordId = IdWorker.getId();
        ExamUserRecord examRecord = new ExamUserRecord()
                .setId(recordId)
                .setUserId(ThreadContext.getUserId())
                .setExamType(req.getExamType())
                .setRelateId(req.getRelateId())
                .setPaperId(paper.getId())
                .setEvaluationMethod(exam.getEvaluationMethod())
                .setExamScoreType(exam.getExamScoreType())
                .setAnswerTime(exam.getAnswerTime())
                .setUserAnswerTime(0).setEndTime(endTime)
                .setTotalScore(paper.getTotalScore())
                .setPassScore(passScore).setFaceId(exam.getFaceId())
                .setFaceIdPass(Boolean.FALSE)
                .setSwitchScreenCount(Boolean.TRUE.equals(exam.getSwitchScreenLimit()) ? exam.getSwitchScreenCount() : -1)
                .setSwitchScreenTime(exam.getSwitchScreenTime())
                .setUserSwitchScreenCount(0);
        examUserRecordDao.save(examRecord);

        // 保存记录答案
        List<ExamUserAnswer> saveUserAnswers = getInitExamUserAnswers(req, startExamResp, recordId, paper);
        if (CollectionUtil.isNotEmpty(saveUserAnswers)) {
            examUserAnswerDao.saveBatch(saveUserAnswers);
        }

        // 处理考勤记录
        if (Boolean.TRUE.equals(exam.getFaceId())) {
            userAttendanceDao.lambdaUpdate()
                    .eq(UserAttendance::getUserId, ThreadContext.getUserId())
                    .eq(UserAttendance::getAttendanceType, AttendanceTypeEnum.GENERAL_EXAM.getCode())
                    .eq(UserAttendance::getAttendanceExamScene, AttendanceExamSceneEnum.EXAM_BEFORE.getCode())
                    .eq(UserAttendance::getRelateId, exam.getId())
                    .eq(UserAttendance::getAttendancePass, Boolean.TRUE)
                    .and(q -> q
                            .isNull(UserAttendance::getExamRecordId)
                            .or()
                            .eq(UserAttendance::getExamRecordId, 0L)
                    )
                    .set(UserAttendance::getExamRecordId, recordId).update();
        }

        // 返回前端试卷信息
        startExamResp.setRecordId(recordId);
        startExamResp.setExamType(req.getExamType());
        startExamResp.setExamName(exam.getExamName());
        startExamResp.setDescription(exam.getDescription());
        startExamResp.setAnswerTime(exam.getAnswerTime());
        startExamResp.setFaceId(exam.getFaceId());
        startExamResp.setSwitchScreenLimit(exam.getSwitchScreenLimit());
        startExamResp.setSwitchScreenCount(exam.getSwitchScreenCount());
        startExamResp.setSwitchScreenTime(exam.getSwitchScreenTime());
        startExamResp.setPassScore(passScore);

        // 更新考试人数
        threadPoolTaskExecutor.execute(() -> {
            // 统计学习人数
            long countStudy = examUserDao.lambdaQuery()
                    .eq(ExamUser::getExamType, exam.getExamType())
                    .eq(ExamUser::getExamId, exam.getId())
                    .count();

            dao.lambdaUpdate()
                    .eq(Exam::getId, exam.getId())
                    .set(Exam::getCountStudy, countStudy)
                    .update();
        });

        return Result.success(startExamResp);
    }

    /**
     * 正式考试
     *
     * @param req 开始考试参数
     * @return 开考响应参数
     */
    private Result<AuthExamStartResp> registrationExamStart(AuthExamStartReq req) {
        // 获取模拟试卷信息
        Exam exam = dao.getById(req.getRelateId());
        if (ObjectUtil.isNull(exam) || !ExamTypeEnum.EXAMINATION.getCode().equals(exam.getExamType())) {
            return Result.error("考试不存在");
        }

        if (!PublishStatusEnum.PUBLISHED.getCode().equals(exam.getPublishStatus())) {
            return Result.error("考试未发布");
        }

        if (LocalDateTime.now().isBefore(exam.getExamStartTime())) {
            // 考试还没开始
            return Result.error("考试时间还没开始，请耐心等待");
        }

        // 考试迟到限时设置
        if (exam.getAllowableLate() && LocalDateTime.now().isAfter(exam.getExamStartTime().plusMinutes(exam.getAllowableLateTime()))) {
            // 当前时间在设定的可迟到时间之后,则不能进入考试
            return Result.error("已超过最迟进入考试时间，无法进入考试");
        }

        ExamUser examUser = examUserDao.getByExamIdAndUserId(exam.getId(), ThreadContext.getUserId());
        if (ObjectUtil.isNull(examUser)) {
            return Result.error("考试用户不存在");
        }
        if (exam.getFaceId() && examUser.getFaceIdErrorCount() >= exam.getFaceIdLimitCount()) {
            return Result.error("作答前人脸认证失败，暂时无法进入考试");
        }
        if (examUser.getExamRecordId() != 0L) {
            return Result.error("已存在考试记录，请不要重复开始考试");
        }

        // 处理人脸校验
        if (exam.getFaceId()) {
            boolean checkResult = userAttendanceDao.lambdaQuery()
                    .eq(UserAttendance::getUserId, ThreadContext.getUserId())
                    .eq(UserAttendance::getAttendanceType, AttendanceTypeEnum.REGISTRATION_EXAM.getCode())
                    .eq(UserAttendance::getAttendanceExamScene, AttendanceExamSceneEnum.EXAM_BEFORE.getCode())
                    .eq(UserAttendance::getRelateId, exam.getId())
                    .eq(UserAttendance::getAttendancePass, Boolean.TRUE)
                    .and(q -> q
                            .isNull(UserAttendance::getExamRecordId)
                            .or()
                            .eq(UserAttendance::getExamRecordId, 0L)
                    )
                    .exists();
            if (!checkResult) {
                return Result.error("考试人脸识别未通过");
            }
        }

        // 获取试卷信息
        Paper paper = paperDao.getByIdForCache(exam.getPaperId());
        if (ObjectUtil.isNull(paper)) {
            return Result.error("试卷不存在");
        }

        ExamCommonResp examDetail = examCommonBiz.getExamDetail(paper);
        AuthExamStartResp startExamResp = BeanUtil.copyProperties(examDetail, AuthExamStartResp.class);

        LocalDateTime endTime = exam.getExamStartTime().plusMinutes(exam.getAnswerTime());

        BigDecimal passScore = paper.getTotalScore()
                .multiply(BigDecimal.valueOf(exam.getPassPercentage()))
                .divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);

        // 保存用户考试记录
        Long recordId = IdWorker.getId();
        ExamUserRecord examRecord = new ExamUserRecord().setId(recordId).setUserId(ThreadContext.getUserId()).setExamType(req.getExamType())
                .setRelateId(req.getRelateId())
                .setPaperId(paper.getId())
                .setEvaluationMethod(exam.getEvaluationMethod())
                .setExamScoreType(exam.getExamScoreType())
                .setAnswerTime(exam.getAnswerTime())
                .setUserAnswerTime(0)
                .setEndTime(endTime)
                .setTotalScore(paper.getTotalScore())
                .setPassScore(passScore)
                .setFaceId(exam.getFaceId())
                .setFaceIdPass(Boolean.FALSE)
                .setSwitchScreenCount(Boolean.TRUE.equals(exam.getSwitchScreenLimit()) ? exam.getSwitchScreenCount() : -1)
                .setSwitchScreenTime(exam.getSwitchScreenTime())
                .setUserSwitchScreenCount(0);
        examUserRecordDao.save(examRecord);

        // 更新考试用户-记录ID
        examUserDao.lambdaUpdate()
                .eq(ExamUser::getId, examUser.getId())
                .set(ExamUser::getExamRecordId, recordId)
                .set(ExamUser::getExamStartTime, LocalDateTime.now())
                .set(ExamUser::getExamEndTime, examRecord.getEndTime())
                .update();

        // 保存记录答案
        List<ExamUserAnswer> saveUserAnswers = getInitExamUserAnswers(req, startExamResp, recordId, paper);
        if (CollectionUtil.isNotEmpty(saveUserAnswers)) {
            examUserAnswerDao.saveBatch(saveUserAnswers);
        }

        // 处理人脸校验，跟具体的考试记录进行关联
        if (Boolean.TRUE.equals(exam.getFaceId())) {
            userAttendanceDao.lambdaUpdate()
                    .eq(UserAttendance::getUserId, ThreadContext.getUserId())
                    .eq(UserAttendance::getAttendanceType, AttendanceTypeEnum.REGISTRATION_EXAM.getCode())
                    .eq(UserAttendance::getAttendanceExamScene, AttendanceExamSceneEnum.EXAM_BEFORE.getCode())
                    .eq(UserAttendance::getRelateId, exam.getId())
                    .eq(UserAttendance::getAttendancePass, Boolean.TRUE)
                    .and(q -> q
                            .isNull(UserAttendance::getExamRecordId)
                            .or()
                            .eq(UserAttendance::getExamRecordId, 0L)
                    )
                    .set(UserAttendance::getExamRecordId, recordId)
                    .update();
        }

        // 返回前端试卷信息
        startExamResp.setRecordId(recordId)
                .setExamType(req.getExamType())
                .setExamName(exam.getExamName())
                .setDescription(exam.getDescription())
                .setAnswerTime(exam.getAnswerTime())
                .setFaceId(exam.getFaceId())
                .setSwitchScreenLimit(exam.getSwitchScreenLimit())
                .setSwitchScreenCount(exam.getSwitchScreenCount())
                .setSwitchScreenTime(exam.getSwitchScreenTime())
                .setPassScore(passScore);

        // 更新考试人数
        threadPoolTaskExecutor.execute(() -> {
            // 统计学习人数
            long countStudy = examUserDao.lambdaQuery()
                    .eq(ExamUser::getExamType, exam.getExamType())
                    .eq(ExamUser::getExamId, exam.getId())
                    .count();

            dao.lambdaUpdate()
                    .eq(Exam::getId, exam.getId())
                    .set(Exam::getCountStudy, countStudy)
                    .update();
        });
        return Result.success(startExamResp);
    }

    /**
     * 获取初始化考试用户答案
     *
     * @param req           开考参数
     * @param startExamResp 开考响应参数
     * @param recordId      考试记录ID
     * @param paper         市局信息
     * @return 考试用户答案列表
     */
    private static List<ExamUserAnswer> getInitExamUserAnswers(AuthExamStartReq req, AuthExamStartResp startExamResp, Long recordId, Paper paper) {
        List<ExamUserAnswer> saveUserAnswers = new ArrayList<>();
        for (AuthExamStartResp.ExamTitle title : startExamResp.getTitles()) {
            for (AuthExamStartResp.Question question : title.getQuestions()) {
                ExamUserAnswer userAnswer = new ExamUserAnswer()
                        .setId(IdWorker.getId())
                        .setUserId(ThreadContext.getUserId())
                        .setRecordId(recordId)
                        .setExamType(req.getExamType())
                        .setRelateId(req.getRelateId())
                        .setPaperId(paper.getId())
                        .setTitleId(title.getId())
                        .setQuestionId(question.getQuestionId())
                        .setQuestionType(question.getQuestionType())
                        .setQuestionScore(question.getQuestionScore())
                        .setUserAnswer("")
                        .setUserScore(BigDecimal.ZERO)
                        .setEvaluationComplete(Boolean.FALSE)
                        .setStatus(StatusEnum.NORMAL.getCode())
                        .setSort(question.getSort());
                saveUserAnswers.add(userAnswer);
            }
        }
        return saveUserAnswers;
    }

    /**
     * 考试-继续考试
     *
     * @param req 考试方法
     * @return 继续考试信息
     */
    public Result<AuthExamContinueResp> examContinue(AuthExamContinueReq req) {
        ExamUserRecord examUserRecord = examUserRecordDao.getById(req.getRecordId());
        if (ObjectUtil.isNull(examUserRecord) || !examUserRecord.getUserId().equals(ThreadContext.getUserId())) {
            return Result.error("考试用户记录不存在");
        }
        // 考试时间加答卷时间在当前时间之后则考试结束
        if (examUserRecord.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.error("考试已经结束");
        }

        // 获取模拟试卷信息
        String examName = "";
        String description = "";
        Integer answerTime = 0;
        Boolean faceId = Boolean.FALSE;
        // 切屏限制
        Boolean switchScreenLimit = Boolean.FALSE;
        // 切屏限制次数
        Integer switchScreenCount = 0;
        // 切屏判断时间(单位：秒)
        Integer switchScreenTime = 0;

        if (ExamTypeEnum.MOCK.getCode().equals(examUserRecord.getExamType()) || ExamTypeEnum.EXAMINATION.getCode().equals(examUserRecord.getExamType())) {
            Exam exam = dao.getById(examUserRecord.getRelateId());
            if (ObjectUtil.isNull(exam) || !ExamTypeEnum.MOCK.getCode().equals(exam.getExamType())) {
                return Result.error("考试不存在");
            }

            examName = exam.getExamName();
            description = exam.getDescription();
            answerTime = exam.getAnswerTime();
            faceId = exam.getFaceId();
            switchScreenLimit = exam.getSwitchScreenLimit();
            switchScreenCount = exam.getSwitchScreenCount();
            switchScreenTime = exam.getSwitchScreenTime();
        }


        Paper paper = paperDao.getByIdForCache(examUserRecord.getPaperId());
        if (ObjectUtil.isNull(paper)) {
            return Result.error("试卷不存在");
        }

        ExamCommonResp examDetail = examCommonBiz.getExamDetail(paper, examUserRecord.getId());
        AuthExamContinueResp resp = BeanUtil.copyProperties(examDetail, AuthExamContinueResp.class);
        resp.setRecordId(examUserRecord.getId());
        resp.setExamType(examUserRecord.getExamType());
        resp.setExamName(examName);
        resp.setDescription(description);
        resp.setAnswerTime(answerTime);
        resp.setFaceId(faceId);
        resp.setSwitchScreenLimit(switchScreenLimit);
        resp.setSwitchScreenCount(switchScreenCount);
        resp.setSwitchScreenTime(switchScreenTime);
        resp.setPassScore(examUserRecord.getPassScore());

        // 计算剩余时间
        long seconds = Duration.between(LocalDateTime.now(), examUserRecord.getEndTime()).getSeconds();
        resp.setSurplusTime((int) seconds);
        return Result.success(resp);
    }

    // TODO 后面最总确定修改为异步再删除
//    public Result<AuthExamEndResp> examEnd(AuthExamEndReq req) {
    public Result<String> examEnd(AuthExamEndReq req) {
        RLock lock = redissonClient.getLock(RedisConstant.Exam.EXAM_END + RedisConstant.SEPARATOR + req.getRecordId());
        try {
            boolean res = lock.tryLock(RedisConstant.DEFAULT_WAIT_TIME, RedisConstant.DEFAULT_LEASE_TIME, TimeUnit.SECONDS);
            if (!res) {
                log.error("用户：{}处理考试记录：{}获取锁等待超时", ThreadContext.getUserId(), req.getRecordId());
                return Result.error(toI18nFail());
            }

            // 获取考试记录
            ExamUserRecord examUserRecord = examUserRecordDao.getById(req.getRecordId());
            if (ObjectUtil.isNull(examUserRecord) || !ThreadContext.getUserId().equals(examUserRecord.getUserId())) {
                return Result.error("考试用户记录不存在");
            }

            if (!ExamStatusEnum.NOT_OVER.getCode().equals(examUserRecord.getExamStatus())) {
                // 不是考试中直接返回
                throw new BaseException("考试已经结束");
            }
            Paper paper = paperDao.getByIdForCache(examUserRecord.getPaperId());
            if (ObjectUtil.isEmpty(paper)) {
                return Result.error("试卷不存在");
            }

            // 处理考试记录
//            return examCommonBiz.handleFinishExam(paper, examUserRecord);
            threadPoolTaskExecutor.execute(() -> examCommonBiz.handleFinishExam(examUserRecord));
            examUserRecordDao.lambdaUpdate()
                    .eq(ExamUserRecord::getId, examUserRecord.getId())
                    .set(ExamUserRecord::getExamStatus, ExamStatusEnum.WAIT_CORRECTED.getCode())
                    .update();
            return Result.success(toI18nSuccess());
        } catch (InterruptedException e) {
            log.error("用户：{}处理考试记录：{}处理失败，\n", ThreadContext.getUserId(), req.getRecordId(), e);
            Thread.currentThread().interrupt();
            throw new BaseException(toI18nFail());
        } finally {
            lock.unlock();
        }
    }

    /**
     * 考试-保存答案
     *
     * @param req 保存答案请求
     * @return 保存结果
     */
    public Result<String> saveAnswer(AuthExamSaveAnswerReq req) {
        // 统计在线考试人数
        cacheCount.setOnlineUserExam(ThreadContext.getUserId());

        // 获取用户考试记录
        ExamUserRecord examUserRecord = examUserRecordDao.getById(req.getRecordId());
        if (ObjectUtil.isNull(examUserRecord) || !ThreadContext.getUserId().equals(examUserRecord.getUserId())) {
            return Result.error("考试用户记录不存在");
        }
        if (!ExamStatusEnum.NOT_OVER.getCode().equals(examUserRecord.getExamStatus()) || examUserRecord.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.error("考试已经结束");
        }

        // 保存用户回答内容
        String userAnswer = StrUtil.isBlank(req.getUserAnswer()) ? "" : req.getUserAnswer();
        boolean updateRes = examUserAnswerDao.lambdaUpdate()
                .eq(ExamUserAnswer::getRecordId, req.getRecordId())
                .eq(ExamUserAnswer::getQuestionId, req.getQuestionId())
                .set(ExamUserAnswer::getUserAnswer, userAnswer)
                .update();
        return updateRes ? Result.success(toI18nSuccess()) : Result.error(toI18nFail());
    }

    /**
     * 考试-开始
     *
     * @param req 考试-更新切屏次数
     * @return 剩余切屏次数
     */
    @Transactional(rollbackFor = Exception.class)
    public Result<Integer> updateSwitchScreenCount(AuthExamUpdateSwitchScreenCountReq req) {
        ExamUserRecord examUserRecord = examUserRecordDao.getById(req.getRecordId());
        if (ObjectUtil.isNull(examUserRecord)) {
            return Result.error("考试用户记录不存在");
        }
        ExamUserRecord updateRecord = new ExamUserRecord();
        updateRecord.setId(examUserRecord.getId());
        updateRecord.setUserSwitchScreenCount(examUserRecord.getUserSwitchScreenCount() + 1);
        examUserRecordDao.updateById(updateRecord);

        if (examUserRecord.getExamType().equals(ExamTypeEnum.EXAMINATION.getCode())) {
            // 正式考试同步修改切屏次数
            ExamUser examUser = examUserDao.getByExamIdAndUserId(examUserRecord.getRelateId(), examUserRecord.getUserId());
            if (ObjectUtil.isNotNull(examUser)) {
                ExamUser updateExamUser = new ExamUser();
                updateExamUser.setId(examUser.getId());
                updateExamUser.setUserSwitchScreenCount(examUser.getUserSwitchScreenCount() + 1);
                examUserDao.updateById(updateExamUser);
            }
        }

        // 返回剩余切屏次数
        int count;
        if (examUserRecord.getSwitchScreenCount() < 0) {
            count = examUserRecord.getSwitchScreenCount();
        } else {
            count = Math.max(examUserRecord.getSwitchScreenCount() - updateRecord.getUserSwitchScreenCount(), 0);
        }
        return Result.success(count);
    }

    /**
     * 考试-考试报告
     *
     * @param recordId 考试记录ID
     * @return 考试报告
     */
    public Result<AuthExamReportResp> report(Long recordId) {
        // 获取用户考试记录
        ExamUserRecord examUserRecord = examUserRecordDao.getById(recordId);
        if (ObjectUtil.isNull(examUserRecord) || !ThreadContext.getUserId().equals(examUserRecord.getUserId())) {
            return Result.error("考试用户记录不存在");
        }
        if (ExamStatusEnum.NOT_OVER.getCode().equals(examUserRecord.getExamStatus())) {
            return Result.error("正在考试中");
        }


        // 获取试卷
        Paper paperInfo = paperDao.getById(examUserRecord.getPaperId());
        if (ObjectUtil.isNull(paperInfo)) {
            return Result.error("试卷不存在");
        }

        AuthExamReportResp resp = BeanUtil.copyProperties(examUserRecord, AuthExamReportResp.class);
        resp.setRecordId(recordId);
        resp.setTotalScore(examUserRecord.getTotalScore());
        resp.setQuestionCount(paperInfo.getQuestionCount());

        // 获取历史最高分
        resp.setHistoryHighestScore(examCommonBiz.getHighestScore(examUserRecord));
        List<ExamUserAnswer> examUserAnswerList = examUserAnswerDao.listByRecordId(examUserRecord.getId());
        if (CollectionUtil.isNotEmpty(examUserAnswerList)) {
            List<ExamUserAnswer> answerList = examUserAnswerList.stream()
                    .filter(ExamUserAnswer::getEvaluationComplete)
                    .filter(item -> BigDecimal.ZERO.compareTo(item.getUserScore()) < 0)
                    .collect(Collectors.toList());
            if (CollectionUtil.isNotEmpty(answerList)) {
                resp.setQuestionCorrectCount(answerList.size());
            } else {
                resp.setQuestionCorrectCount(0);
            }

            long answerCount = examUserAnswerList.stream()
                    .filter(ExamUserAnswer::getEvaluationComplete)
                    .count();
            resp.setQuestionErrorCount((int) answerCount - resp.getQuestionCorrectCount());

            List<ExamUserAnswer> subjectiveQuestionList = answerList.stream()
                    .filter(item -> item.getQuestionType().equals(QuestionTypeEnum.GAP_FILLING.getCode()) || item.getQuestionType()
                            .equals(QuestionTypeEnum.SHORT_ANSWER.getCode())).collect(Collectors.toList());
            if (CollectionUtil.isNotEmpty(subjectiveQuestionList)) {
                BigDecimal subjectiveQuestionScore = subjectiveQuestionList.stream().map(ExamUserAnswer::getUserScore).reduce(BigDecimal.ZERO, BigDecimal::add);
                resp.setSubjectiveQuestionScore(subjectiveQuestionScore);
                resp.setObjectiveQuestionScore(NumberUtil.sub(examUserRecord.getUserScore(), resp.getSubjectiveQuestionScore()));
            } else {
                BigDecimal objectiveQuestionScore = answerList.stream().map(ExamUserAnswer::getUserScore).reduce(BigDecimal.ZERO, BigDecimal::add);
                resp.setObjectiveQuestionScore(objectiveQuestionScore);
            }
        }

        // 显示答案规则
        ExamCommonBiz.ExamShowRule showAnswerRule = examCommonBiz.getShowAnswerRule(examUserRecord);
        if (ObjectUtil.isNotNull(showAnswerRule)) {
            resp.setResultShow(showAnswerRule.getResultShow());
            resp.setResultShowRule(showAnswerRule.getResultShowRule());
            resp.setResultShowTime(showAnswerRule.getResultShowTime());
            resp.setAnswerShow(showAnswerRule.getAnswerShow());
            resp.setAnswerShowRule(showAnswerRule.getAnswerShowRule());
            resp.setResultShowRank(showAnswerRule.getResultShowRank());
            resp.setRankList(showAnswerRule.getRankList());

            // 排名
            if (Boolean.TRUE.equals(showAnswerRule.getResultShowRank())) {
                Integer rankNo = examUserDao.getRankNoByExamTypeAndExamIdAndUserId(examUserRecord.getExamType(), examUserRecord.getRelateId(), examUserRecord.getUserId());
                resp.setRankNo(rankNo);
            }
            //判断分数显示规则
            if (ObjectUtil.isNotNull(resp.getResultShow())) {
                boolean checkRes = Boolean.TRUE;
                if (Boolean.FALSE.equals(resp.getResultShow())) {
                    checkRes = Boolean.FALSE;
                } else {
                    if (ResultShowRuleEnum.END.getCode().equals(resp.getResultShowRule())) {
                        checkRes = LocalDateTime.now().isAfter(examUserRecord.getEndTime());
                    } else if (ResultShowRuleEnum.SUBMIT.getCode().equals(resp.getResultShowRule())) {
                        checkRes = Boolean.TRUE;
                    } else if (ResultShowRuleEnum.CUSTOM.getCode().equals(resp.getResultShowRule())) {
                        checkRes = LocalDateTime.now().isAfter(resp.getResultShowTime());
                    }
                }

                //不显示分数或者未到达显示时间，则把对应分数设置为0
                if (!checkRes) {
                    resp.setUserScore(null);
                    resp.setHistoryHighestScore(BigDecimal.ZERO);
                    resp.setSubjectiveQuestionScore(BigDecimal.ZERO);
                    resp.setObjectiveQuestionScore(BigDecimal.ZERO);
                }
            }
        }

        // 校验是否可以继续考试
        ExamCommonBiz.ExamCheckContinue examCheckContinue = examCommonBiz.checkCanContinueExam(examUserRecord.getUserId(), examUserRecord.getExamType(), examUserRecord.getRelateId());
        resp.setStartExam(examCheckContinue.getStartExam());
        resp.setResidueExamCount(examCheckContinue.getResidueExamCount());
        return Result.success(resp);
    }

    /**
     * 考试-作答解析
     *
     * @param recordId 考试记录ID
     * @return 考试作答解析
     */
    public Result<AuthExamParseResp> parse(Long recordId) {
        // 获取用户考试记录
        ExamUserRecord examUserRecord = examUserRecordDao.getById(recordId);
        if (ObjectUtil.isNull(examUserRecord) || !ThreadContext.getUserId().equals(examUserRecord.getUserId())) {
            return Result.error("考试用户记录不存在");
        }
        if (ExamStatusEnum.NOT_OVER.getCode().equals(examUserRecord.getExamStatus())) {
            return Result.error("正在考试中");
        }

        // 获取试卷
        Paper paperInfo = paperDao.getById(examUserRecord.getPaperId());
        if (ObjectUtil.isNull(paperInfo)) {
            return Result.error("试卷不存在");
        }
        ExamCommonBiz.ExamShowRule examShowRule = examCommonBiz.getShowAnswerRule(examUserRecord);


        // 处理试题解析和用户答案
        AuthExamParseResp resp = BeanUtil.copyProperties(examUserRecord, AuthExamParseResp.class);
        resp.setUserScore(examUserRecord.getUserScore());
        resp.setQuestionCount(paperInfo.getQuestionCount());
        resp.setTotalScore(examUserRecord.getTotalScore());
        resp.setPassScore(examUserRecord.getPassScore());
        resp.setPassStatus(examUserRecord.getPassStatus());

        // 处理显示规则
        if (ObjectUtil.isNotNull(examShowRule)) {
            resp.setResultShow(examShowRule.getResultShow());
            resp.setResultShowRule(examShowRule.getResultShowRule());
            resp.setResultShowTime(examShowRule.getResultShowTime());
            resp.setAnswerShow(examShowRule.getAnswerShow());
            resp.setAnswerShowRule(examShowRule.getAnswerShowRule());
            resp.setResultShowRank(examShowRule.getResultShowRank());
            resp.setRankList(examShowRule.getRankList());
        } else {
            resp.setResultShow(Boolean.FALSE);
            resp.setAnswerShow(Boolean.FALSE);
            resp.setResultShowRank(Boolean.FALSE);
            resp.setRankList(Boolean.FALSE);
        }

        // 获取考试名称
        String examName = examCommonBiz.getExamName(examUserRecord.getExamType(), examUserRecord.getRelateId());
        resp.setExamName(examName);

        Users user = usersDao.getById(ThreadContext.getUserId());
        if (ObjectUtil.isNotNull(user)) {
            resp.setNickname(user.getNickname());
        }

        // 获取试题信息
        List<AuthExamParseResp.ExamTitle> titleRespList = getExamUserPaper(examUserRecord, examShowRule);
        resp.setTitles(titleRespList);
        return Result.success(resp);
    }

    /**
     * 获取用户考试试卷
     *
     * @param examUserRecord 考试记录
     * @param showAnswerRule 展示答案规则
     * @return 试卷信息
     */
    private List<AuthExamParseResp.ExamTitle> getExamUserPaper(ExamUserRecord examUserRecord, ExamCommonBiz.ExamShowRule showAnswerRule) {
        // 获取用户考试答案
        List<ExamUserAnswer> answerList = examUserAnswerDao.listByRecordId(examUserRecord.getId());
        Map<Long, List<ExamUserAnswer>> paperAnswerMap = answerList.stream().collect(Collectors.groupingBy(ExamUserAnswer::getTitleId));

        // 获取试卷题目
        Set<Long> questionIds = answerList.stream().map(ExamUserAnswer::getQuestionId).collect(Collectors.toSet());
        List<QuestionDetailResp> questionList = questionCommonBiz.listDetailByIds(questionIds);
        Map<Long, QuestionDetailResp> questionMap = questionList.stream().collect(Collectors.toMap(QuestionDetailResp::getId, Function.identity()));

        // 获取试卷标题
        Set<Long> titleIds = answerList.stream().map(ExamUserAnswer::getTitleId).collect(Collectors.toSet());
        List<PaperTitle> paperTitleList = paperTitleDao.sortListByIds(titleIds);
        if (CollectionUtil.isEmpty(paperTitleList)) {
            return Collections.emptyList();
        }

        // 补充数据
        List<AuthExamParseResp.ExamTitle> titleRespList = BeanUtil.copyProperties(paperTitleList, AuthExamParseResp.ExamTitle.class);
        for (AuthExamParseResp.ExamTitle titleResp : titleRespList) {
            List<ExamUserAnswer> paperAnswerList = paperAnswerMap.get(titleResp.getId());

            // 收集标题下的试题
            List<AuthExamParseResp.Question> respTitleQuestions = new ArrayList<>();
            for (ExamUserAnswer paperAnswer : paperAnswerList) {
                AuthExamParseResp.Question respQuestion = new AuthExamParseResp.Question()
                        .setQuestionId(paperAnswer.getQuestionId())
                        .setQuestionType(paperAnswer.getQuestionType())
                        .setQuestionScore(paperAnswer.getQuestionScore())
                        .setUserAnswer(paperAnswer.getUserAnswer())
                        .setUserScore(paperAnswer.getUserScore())
                        .setEvaluationComment(paperAnswer.getEvaluationComment())
                        .setSort(paperAnswer.getSort())
                        .setEvaluationComplete(paperAnswer.getEvaluationComplete());


                QuestionDetailResp question = questionMap.get(paperAnswer.getQuestionId());
                if (ObjectUtil.isNull(question)) {
                    continue;
                }

                respQuestion.setQuestionTitle(question.getQuestionTitle());
                respQuestion.setQuestionAnswer(question.getQuestionAnswer());
                respQuestion.setQuestionAnswerIds(question.getQuestionAnswerIds());
                respQuestion.setDifficultyLevel(question.getDifficultyLevel());
                respQuestion.setOptionCount(question.getOptionCount());
                respQuestion.setOptions(BeanUtil.copyProperties(question.getOptions(), AuthExamParseResp.QuestionOption.class));

                // 判断如果是批改中状态
                Integer userAnswerStatus;
                if (Boolean.FALSE.equals(paperAnswer.getEvaluationComplete())) {
                    userAnswerStatus = UserAnswerStatusEnum.WAIT_CORRECTED.getCode();
                } else {
                    userAnswerStatus = BigDecimal.ZERO.compareTo(respQuestion.getUserScore()) < 0 ? UserAnswerStatusEnum.CORRECT.getCode() : UserAnswerStatusEnum.ERROR.getCode();
                }

                // 是否展示解析
                respQuestion.setQuestionAnalysis("");
                respQuestion.setQuestionAnswer("");
                respQuestion.setQuestionAnswerIds(Collections.emptyList());
                respQuestion.setUserAnswerStatus(UserAnswerStatusEnum.WAIT_CORRECTED.getCode());
                if (ObjectUtil.isNotNull(showAnswerRule)) {
                    if (Boolean.TRUE.equals(showAnswerRule.getAnswerShow())) {
                        respQuestion.setQuestionAnswer(question.getQuestionAnswer());
                        respQuestion.setQuestionAnswerIds(question.getQuestionAnswerIds());
                        respQuestion.setUserAnswerStatus(userAnswerStatus);
                        if (AnswerShowRuleEnum.RIGHT_WRONG_ANALYSIS.getCode().equals(showAnswerRule.getAnswerShowRule())) {
                            respQuestion.setQuestionAnalysis(question.getQuestionAnalysis());
                        }
                    }
                }
                respTitleQuestions.add(respQuestion);
            }
            // 赋值给标题的试题集合
            titleResp.setQuestionCount(respTitleQuestions.size());
            titleResp.setQuestions(respTitleQuestions);
        }
        return titleRespList;
    }

    /**
     * 考试-人脸错误申诉
     *
     * @param req 申诉参数
     * @return 申述结果
     */
    public Result<String> faceErrorAppeal(AuthExamFaceErrorAppealReq req) {
        Exam exam = dao.getById(req.getExamId());
        if (ObjectUtil.isNull(exam) || !ExamTypeEnum.EXAMINATION.getCode().equals(exam.getExamType())) {
            return Result.error("考试不存在");
        }
        ExamUser examUser = examUserDao.getByExamIdAndUserId(req.getExamId(), ThreadContext.getUserId());
        if (ObjectUtil.isNull(examUser)) {
            return Result.error("考试用户不存在");
        }

        // 判断是否已经过了考试时间
        if (Boolean.FALSE.equals(exam.getFaceIdFailureAppeal())) {
            return Result.error("考试人脸识别失败未开启申诉功能，不允许申诉");
        }
        if (exam.getExamStartTime().plusMinutes(exam.getAnswerTime()).isBefore(LocalDateTime.now())) {
            return Result.error("该考试已过期，无法进行申诉");
        }

        RLock lock = redissonClient.getLock(RedisConstant.Exam.EXAM_FACE_ERROR_APPEAL_SUBMIT + ThreadContext.getUserId());
        try {
            boolean res = lock.tryLock(RedisConstant.DEFAULT_WAIT_TIME, RedisConstant.DEFAULT_LEASE_TIME, TimeUnit.SECONDS);
            if (!res) {
                log.error("用户：{}处理考试人脸申诉：{}获取锁等待超时", ThreadContext.getUserId(), req.getExamId());
                return Result.error(toI18nFail());
            }

            // 判断是否存在申请中记录
            boolean passExist = examUserAppealDao.lambdaQuery()
                    .eq(ExamUserAppeal::getExamId, req.getExamId())
                    .eq(ExamUserAppeal::getUserId, ThreadContext.getUserId())
                    .eq(ExamUserAppeal::getAppealStatus, ExamFaceAppealStatusEnum.PASS.getCode())
                    .exists();
            if (passExist) {
                return Result.error("该考试已经申诉成功，无须重复申诉");
            }

            // 判断是否存在申请中记录
            boolean waitExist = examUserAppealDao.lambdaQuery()
                    .eq(ExamUserAppeal::getExamId, req.getExamId())
                    .eq(ExamUserAppeal::getUserId, ThreadContext.getUserId())
                    .eq(ExamUserAppeal::getAppealStatus, ExamFaceAppealStatusEnum.WAIT_AUDIT.getCode())
                    .exists();
            if (waitExist) {
                return Result.error("存在申诉中请求，无须重复申诉");
            }

            ExamUserAppeal errorAppeal = new ExamUserAppeal()
                    .setUserId(ThreadContext.getUserId())
                    .setExamId(exam.getId())
                    .setAppealStatus(ExamFaceAppealStatusEnum.WAIT_AUDIT.getCode())
                    .setAppealCause(req.getAppealCause());
            if (examUserAppealDao.save(errorAppeal)) {
                return Result.success(toI18nSuccess());
            }
            return Result.error(toI18nFail());
        } catch (InterruptedException e) {
            log.error("用户：{}处理考试人脸申诉：{}处理失败，\n", ThreadContext.getUserId(), req.getExamId(), e);
            Thread.currentThread().interrupt();
            throw new BaseException(toI18nFail());
        } finally {
            lock.unlock();
        }
    }

    public Result<Page<AuthExamRecordPageResp>> recordPage(AuthExamRecordPageReq req) {
        IPage<ExamUserRecord> page = examUserRecordDao.lambdaQuery()
                .eq(ExamUserRecord::getExamType, req.getExamType())
                .eq(ExamUserRecord::getRelateId, req.getRelateId())
                .eq(ExamUserRecord::getUserId, ThreadContext.getUserId())
                .orderByDesc(ExamUserRecord::getId)
                .page(PageDTO.of(req.getPageCurrent(), req.getPageSize()));
        Page<AuthExamRecordPageResp> respPage = PageUtil.transform(page, AuthExamRecordPageResp.class);
        return Result.success(respPage);
    }

    /**
     * 考试-考试排行榜分页
     *
     * @param req 排名参数
     * @return 排名结果
     */
    public Result<Page<AuthExamRankPageResp>> rankPage(AuthExamRankPageReq req) {
        if (ExamTypeEnum.MOCK.getCode().equals(req.getExamType())) {
            // 模拟考试
            return examRankPage(req);
        } else if (ExamTypeEnum.EXAMINATION.getCode().equals(req.getExamType())) {
            // 正式考试
            return examRankPage(req);
        } else {
            // 其他类型
            return Result.error("该考试类型暂不支持");
        }
    }

    /**
     * 考试-考试排行榜分页
     *
     * @param req 排名参数
     * @return 排名结果
     */
    private Result<Page<AuthExamRankPageResp>> examRankPage(AuthExamRankPageReq req) {
        // 查询总条数
        Long totalCount = examUserDao.lambdaQuery().eq(ExamUser::getExamId, req.getRelateId()).count();
        if (ObjectUtil.isNull(totalCount) || totalCount <= 0L) {
            return Result.success(new Page<>());
        }

        // 校验当前页
        long pageCurrent = PageUtil.checkCurrent(totalCount, req.getPageSize(), req.getPageCurrent());
        if (pageCurrent != req.getPageCurrent()) {
            return Result.success(new Page<>());
        }

        // 计算总页数
        long totalPage = PageUtil.countTotalSize(totalCount, req.getPageSize());

        // 处理查询条件
        LambdaQueryWrapper<ExamUser> wrapper = new QueryWrapper<ExamUser>().select("user_id as userId", "exam_id as examId", "exam_type as examType", "user_score as userScore", "exam_submit_time as examSubmitTime", "user_answer_time as userAnswerTime", "user_exam_status as userExamStatus", "user_evaluation_status as userEvaluationStatus", "RANK() OVER (ORDER BY user_score DESC) AS rankNo").orderByAsc("rankNo").lambda().eq(ExamUser::getExamType, req.getExamType()).eq(ExamUser::getExamId, req.getRelateId()).last(SqlUtil.limitSql(totalCount, pageCurrent, req.getPageSize()));

        // 查询排名信息
        List<AuthExamRankPageResp> respList = examUserDao.selectObjs(wrapper, AuthExamRankPageResp.class);
        if (CollectionUtil.isEmpty(respList)) {
            return Result.success(new Page<>());
        }

        // 查询用户信息
        Set<Long> userIds = respList.stream().map(AuthExamRankPageResp::getUserId).collect(Collectors.toSet());
        List<Users> usersList = usersDao.listByIds(userIds);
        Map<Long, Users> userMap = new HashMap<>();
        if (CollectionUtil.isEmpty(usersList)) {
            usersList.forEach(item -> userMap.put(item.getId(), item));
        }

        // 处理排名结果
        for (AuthExamRankPageResp resp : respList) {
            if (CollectionUtil.isNotEmpty(userMap)) {
                Users user = userMap.get(resp.getUserId());
                if (ObjectUtil.isNotNull(user)) {
                    resp.setNickname(user.getNickname());
                    resp.setUserHead(user.getUserHead());
                }
            }
        }

        // 处理响应信息
        Page<AuthExamRankPageResp> respPage = new Page<>(respList, totalCount.intValue(), (int) totalPage, req.getPageCurrent(), req.getPageSize());
        return Result.success(respPage);
    }

    public Result<AuthExamRankSelfResp> selfRank(AuthExamRankSelfReq req) {
        Users users = usersDao.getById(ThreadContext.getUserId());

        if (ExamTypeEnum.MOCK.getCode().equals(req.getExamType())) {
            // 模拟考试
            return getExamRankSelf(req, users);
        } else if (ExamTypeEnum.EXAMINATION.getCode().equals(req.getExamType())) {
            // 正式考试
            return getExamRankSelf(req, users);
        } else {
            // 其他类型
            return Result.error("该考试类型暂不支持");
        }
    }

    /**
     * 获取自身考试排名
     *
     * @param req   查询参数
     * @param users 用户信息
     * @return 排名信息
     */
    private Result<AuthExamRankSelfResp> getExamRankSelf(AuthExamRankSelfReq req, Users users) {
        ExamUser examUser = examUserDao.getByExamTypeAndExamIdAndUserId(req.getExamType(), req.getRelateId(), ThreadContext.getUserId());
        if (ObjectUtil.isNull(examUser)) {
            AuthExamRankSelfResp resp = new AuthExamRankSelfResp().setUserId(ThreadContext.getUserId()).setExamId(req.getRelateId()).setExamType(req.getExamType());
            if (ObjectUtil.isNotNull(users)) {
                resp.setUserHead(users.getUserHead());
                resp.setNickname(users.getNickname());
            }
            return Result.success(resp);
        }
        Integer rankNo = examUserDao.getRankNoByExamTypeAndExamIdAndUserId(req.getExamType(), req.getRelateId(), ThreadContext.getUserId());

        AuthExamRankSelfResp resp = BeanUtil.copyProperties(examUser, AuthExamRankSelfResp.class);
        resp.setRankNo(rankNo);
        if (ObjectUtil.isNotNull(users)) {
            resp.setUserHead(users.getUserHead());
            resp.setNickname(users.getNickname());
        }
        return Result.success(resp);
    }

    /**
     * 考试-用户分页
     *
     * @param req 分页查询参数
     * @return 分页结果
     */
    public Result<Page<AuthExamUserPageResp>> userPage(AuthExamUserPageReq req) {
        // 查询考试
        Map<Long, Exam> examMap = new HashMap<>();
        if (ObjectUtil.isNotNull(req.getExamType()) || StrUtil.isNotBlank(req.getExamName())) {
            List<Exam> examList = dao.lambdaQuery().eq(ObjectUtil.isNotNull(req.getExamType()), Exam::getExamType, req.getExamType()).like(StrUtil.isNotBlank(req.getExamName()), Exam::getExamName, req.getExamName()).list();
            if (CollectionUtil.isEmpty(examList)) {
                return Result.success(new Page<>());
            }
            examList.forEach(item -> examMap.put(item.getId(), item));
        }

        // 查询分页
        IPage<ExamUser> page = examUserDao.lambdaQuery().eq(ExamUser::getUserId, ThreadContext.getUserId()).eq(ObjectUtil.isNotNull(req.getExamType()), ExamUser::getExamType, req.getExamType()).in(CollectionUtil.isNotEmpty(examMap), ExamUser::getExamId, examMap.keySet()).orderByDesc(ExamUser::getId).page(PageDTO.of(req.getPageCurrent(), req.getPageSize()));
        Page<AuthExamUserPageResp> respPage = PageUtil.transform(page, AuthExamUserPageResp.class);
        if (ObjectUtil.isNull(respPage) || CollectionUtil.isEmpty(respPage.getList())) {
            return Result.success(respPage);
        }

        if (CollectionUtil.isEmpty(examMap)) {
            Set<Long> examIds = respPage.getList().stream().map(AuthExamUserPageResp::getExamId).collect(Collectors.toSet());
            List<Exam> examList = dao.listByIds(examIds);
            if (CollectionUtil.isNotEmpty(examList)) {
                examList.forEach(item -> examMap.put(item.getId(), item));
            }
        }

        // 获取试卷
        Map<Long, Paper> paperMap = new HashMap<>();
        if (CollectionUtil.isNotEmpty(examMap)) {
            Set<Long> paperIds = examMap.values().stream().map(Exam::getPaperId).collect(Collectors.toSet());
            List<Paper> paperList = paperDao.listByIds(paperIds);
            if (CollectionUtil.isNotEmpty(paperList)) {
                paperList.forEach(item -> paperMap.put(item.getId(), item));
            }
        }

        for (AuthExamUserPageResp resp : respPage.getList()) {
            Exam exam = examMap.get(resp.getExamId());
            if (ObjectUtil.isNull(exam)) {
                continue;
            }
            resp.setExamName(exam.getExamName());

            Paper paper = paperMap.get(exam.getPaperId());
            if (ObjectUtil.isNotNull(paper)) {
                resp.setTotalScore(paper.getTotalScore());

                BigDecimal passScore = paper.getTotalScore().multiply(BigDecimal.valueOf(exam.getPassPercentage())).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
                resp.setPassScore(passScore);
            }
        }
        return Result.success(respPage);
    }
}
